﻿#region Header
// ------------------------ Licence / Copyright ------------------------
// 
// ScrumTable for Scrum Meeting Support
// Copyright © HSR - Hochschule für Technik Rapperswil
// All Rights Reserved
// 
// Author:
//  Michael Gfeller Silvan Gehrig Patrick Boos
// 
// ---------------------------------------------------------------------
#endregion

#region Usings

using System;
using System.Collections;
using System.Collections.Generic;
using System.Net;
using Microsoft.TeamFoundation.WorkItemTracking.Client;

using ScrumTable.Common;
using ScrumTable.Common.Collections;
using ScrumTable.Common.Logging;
using ScrumTable.Common.Monitoring;
using ScrumTable.Common.Net;
using ScrumTable.Common.Security;
using ScrumTable.Common.Text;
using ScrumTable.Common.Threading;
using ScrumTable.DL.Data.Generic.Cache;
using ScrumTable.DL.Data.Schema;
using ScrumTable.DL.Data.TFS.Bulk.Properties;
using ScrumTable.DL.Data.TFS.Services.Reporting;
using ScrumTable.DL.Data.TFS.Services.TeamFoundation;

#endregion

namespace ScrumTable.DL.Data.TFS.Bulk.Services
{
    /// <summary>
    /// This class is used to load all data below a given project.
    /// </summary>
    internal class WorkItemDataLoader : DisposeBase
    {
        #region Declarations
        //--------------------------------------------------------------------
        // Declarations
        //--------------------------------------------------------------------

        // CAUTION: Don't use "History", it's not language independent!
        private const string HistoryFieldName = "System.History"; 

        private readonly CacheElementFactory _elementFactory;

        #endregion

        #region Properties
        //--------------------------------------------------------------------
        // Properties
        //--------------------------------------------------------------------

        #endregion

        #region Constructors / Destructor
        //--------------------------------------------------------------------
        // Constructors / Destructor
        //--------------------------------------------------------------------

        /// <summary>
        /// Creates a new WorkItemDataLoader instance, no public constructor
        /// needed.
        /// </summary>
        internal WorkItemDataLoader(CacheElementFactory elementFactory)
        {
            PreCondition.AssertNotNull(elementFactory, "elementFactory");

            _elementFactory = elementFactory;
        }

        #endregion

        #region Methods
        //--------------------------------------------------------------------
        // Methods
        //--------------------------------------------------------------------

        internal IDictionary<int, WorkItemDataLoadToken<T>> QueryWorkItemTokens<T>(WorkItemStore workItems, CacheProject parentProject, string workItemTypeName)
        {
            PreCondition.AssertNotNull(workItems, "workItems");
            PreCondition.AssertNotNull(parentProject, "parentProject");
            PreCondition.AssertNotNullOrEmpty(workItemTypeName, "workItemTypeName");

            IDictionary<int, WorkItemDataLoadToken<T>> tokenListToFill = new Dictionary<int, WorkItemDataLoadToken<T>>();
            Query toExecute = new Query(
                workItems,
                string.Format(Resources.QueryWorkItems, workItemTypeName),
                new Hashtable { { "project", parentProject.Name } } );

            foreach (WorkItem workItem in toExecute.RunQuery())
            {
                if (!tokenListToFill.ContainsKey(workItem.Id))
                {
                    tokenListToFill[workItem.Id] = new WorkItemDataLoadToken<T>(
                        workItem.Id,
                        parentProject.WorkItemTypes.TryGetByName(workItemTypeName));
                }
            }
            return tokenListToFill;
        }

        /// <summary>
        /// Creates a new cache work item using the given original work item id.
        /// </summary>
        /// <param name="workItem">Specifies the fully loaded original work item.</param>
        /// <param name="type">Specifies the type of the original work item.</param>
        /// <returns>Returns the created work item.</returns>
        internal CacheWorkItem LoadWorkItem(WorkItem workItem, CacheWorkItemType type)
        {
            PreCondition.AssertNotNull(workItem, "workItem");
            PreCondition.AssertNotNull(type, "type");

            if (ValidateWorkItemType(workItem, type))
            {
                TfsLinkHandler linkHandler = new TfsLinkHandler(workItem);
                
                CacheWorkItem newWorkItem = _elementFactory.CreateWorkItem(
                    type,
                    workItem.Id,
                    workItem.TryGetValue<string>(type.NameField.Schema, string.Empty),
                    workItem.Revision,
                    (workItem.IterationId != workItem.Project.Id) ? workItem.IterationId : CacheIdentity.DefaultId,
                    workItem.TryGetValue<string>(type.StateField.Schema, string.Empty),
                    workItem.TryGetValue<string>(type.DescriptionField.Schema, string.Empty),
                    workItem.TryGetValue<string>(type.MemberIdField.Schema, string.Empty),
                    LoadValues(workItem, type),
                    (type.IsBug || type.IsTask) ? LoadParentLink(linkHandler) : new CacheWorkItemLink[0],
                    (type.IsUserStory) ? LoadChildLinks(linkHandler) : new CacheWorkItemLink[0],
                    LoadAttachments(workItem),
                    LoadRevisions(workItem));

                newWorkItem.ImportSettings(LoadSettingsText(type, workItem.Revisions));
                return newWorkItem;
            }
            return null;
        }

        /// <summary>
        /// Creates a new cache work item using the given original work item id.
        /// </summary>
        /// <param name="wiStore">Specifies the original work item store.</param>
        /// <param name="workItemId">Specifies the id of the original work item.</param>
        /// <param name="type">Specifies the type of the original work item.</param>
        /// <returns>Returns the created work item.</returns>
        internal CacheWorkItem LoadWorkItemById(WorkItemStore wiStore, int workItemId, CacheWorkItemType type)
        {
            PreCondition.AssertNotNull(wiStore, "wiStore");
            PreCondition.AssertNotNull(type, "type");

            //WorkItem wi = new WorkItem();
            //// TODO: REMOVE THIS HACK [for TFS 2012 - there's a Bug in TFS API when loading data over the WorkItem's constructor]
            //// force load all data
            //Query toExecute = new Query(
            //    wiStore,
            //    string.Format(Resources.QueryAllWorkItemData, type.Name, workItemId),
            //    new Hashtable { { "project", type.Project.Name } });
            //WorkItemCollection workItems = toExecute.RunQuery();

            //if (workItems.Count > 0)
            //{
            //    return LoadWorkItem(workItems[0], type);
            //}

            WorkItem toLoad = wiStore.GetWorkItem(workItemId);
            toLoad.Open(); // open WI to make sure the element is loaded completly and changes can be stored back to the server
            return LoadWorkItem(toLoad, type);
        }

        /// <summary>
        /// Creates a new cache work item using the given original work item id.
        /// </summary>
        /// <param name="wiStore">Specifies the original work item store.</param>
        /// <param name="token">Specifies the work item load token which should be filled (it must also contain the id and the type of the original work item).</param>
        /// <returns>Returns the created work item.</returns>
        internal void LoadWorkItemByToken(WorkItemStore wiStore, WorkItemDataLoadToken<CacheWorkItem> token)
        {
            PreCondition.AssertNotNull(wiStore, "wiStore");
            PreCondition.AssertNotNull(token, "token");

            // TODO: REMOVE THIS HACK [for TFS 2012 - there's a Bug in TFS API when loading data over the WorkItem's constructor]
            // force load all data
            //Query toExecute = new Query(
            //    wiStore,
            //    string.Format(Resources.QueryAllWorkItemData, token.Type.Name, token.Reference),
            //    new Hashtable { { "project", token.Type.Project.Name } });
            //WorkItemCollection workItems = toExecute.RunQuery();
            
            //if (workItems.Count > 0)
            //{
            //    token.Result = LoadWorkItem(workItems[0], token.Type);
            //}

            WorkItem toLoad = wiStore.GetWorkItem(token.Reference);
            toLoad.Open(); // open WI to make sure the element is loaded completly and changes can be stored back to the server
            token.Result = LoadWorkItem(toLoad, token.Type);
        }


        #region Settings Handling Methods

        /// <summary>
        /// Parses the settings stored in the given revision tree.
        /// </summary>
        /// <param name="type">Specifies the work item type which contains the fields and their back end types.</param>
        /// <param name="revisions">Specifies the revisions which contain the workitem settings text.</param>
        /// <returns>Returns the parsed settings file content.</returns>
        private IDictionary<string, ICollection<object>> LoadSettingsText(CacheWorkItemType type, IList revisions)
        {
            PreCondition.AssertNotNull(revisions, "revisions");

            int lastHistoryIdx = IndexOfLastScrumTableSettingsText(revisions);

            if (lastHistoryIdx != -1)
            {
                string revHistoryValue = (string)((Revision)revisions[lastHistoryIdx]).Fields[HistoryFieldName].Value;
                IDictionary<string, ICollection<string>> historySettingsValues = ParseLastScrumTableSettingsText(revHistoryValue);

                RemoveObsolteScrumTableSettings(
                    revisions,
                    lastHistoryIdx,
                    historySettingsValues);

                return ConvertHistoryValues(type, historySettingsValues);
            }
            return new Dictionary<string, ICollection<object>>();
        }

        private IDictionary<string, ICollection<object>> ConvertHistoryValues(CacheWorkItemType type, IDictionary<string, ICollection<string>> elementsToConvert)
        {
            IDictionary<string, ICollection<object>> convertedValues = new Dictionary<string, ICollection<object>>(elementsToConvert.Count);

            foreach (KeyValuePair<string, ICollection<string>> fieldValues in elementsToConvert)
            {
                CacheFieldDefinition field = type.TryGetFieldByBackEndId(fieldValues.Key);

                if (field != null)
                {
                    convertedValues[fieldValues.Key] = fieldValues.Value.Convert(
                        data => (!string.IsNullOrEmpty(data))
                                    ? ObjectUtil.ConvertFromString(data, field.BackEndFieldType)
                                    : field.BackEndFieldType.GetDefault());
                }
            }
            return convertedValues;
        }

        private IDictionary<string, ICollection<string>> ParseLastScrumTableSettingsText(string revisionText)
        {
            string lastHistory = revisionText;
            string toParse = lastHistory.Substring(
                Resources.DatSvProcSaveHistoryBegin.Length,
                lastHistory.Length - Resources.DatSvProcSaveHistoryBegin.Length - Resources.DatSvProcSaveHistoryEnd.Length);

            return new IniParser().Parse(toParse, ValueParser.Default.Parse);
        }

        private int IndexOfLastScrumTableSettingsText(IList revisions)
        {
            int lastHistoryIdx = -1;
            int historyIdx = -1;

            foreach (Revision rev in revisions)
            {
                ++historyIdx;

                string revTagLine = rev.Fields[HistoryFieldName].Value as string;

                if (revTagLine != null
                    && revTagLine.StartsWith(Resources.DatSvProcSaveHistoryBegin)
                    && revTagLine.EndsWith(Resources.DatSvProcSaveHistoryEnd))
                {
                    lastHistoryIdx = historyIdx;
                }
            }
            return lastHistoryIdx;
        }

        private void RemoveObsolteScrumTableSettings(IList revisions, int historyIndex, IDictionary<string, ICollection<string>> historySettingsValues)
        {
            // remove a settings values if it changed in meantime
            for (int i = historyIndex + 1; i < revisions.Count; ++i)
            {
                foreach (Field revField in ((Revision)revisions[i]).Fields)
                {
                    if (historySettingsValues.ContainsKey(revField.ReferenceName))
                    {
                        historySettingsValues.Remove(revField.ReferenceName);
                    }
                }
            }
        }

        #endregion


        private bool ValidateWorkItemType(WorkItem workItem, CacheWorkItemType expectedType)
        {
            return (workItem.Type == workItem.Project.WorkItemTypes[expectedType.Name]);
        }

        private IList<CacheAttachment> LoadAttachments(WorkItem workItem)
        {
            return workItem.Attachments.Convert<CacheAttachment, Attachment>(
                atta => _elementFactory.CreateAttachment(atta.Name, TfsAttachment.TryGetIdFromAttachmentUri(atta), atta.Comment, atta.Uri));
        }

        private IList<CacheWorkItemRevision> LoadRevisions(WorkItem workItem)
        {
            return workItem.Revisions.Convert<CacheWorkItemRevision, Revision>(
                rev => _elementFactory.CreateRevision(rev.Index, rev.GetTagLine(), rev.Fields[HistoryFieldName].Value as string));
        }

        private IList<CacheWorkItemLink> LoadChildLinks(TfsLinkHandler handler)
        {
            return handler.ChildLinks.Convert(l => _elementFactory.CreateWorkItemLink(l.TargetId));
        }

        private IList<CacheWorkItemLink> LoadParentLink(TfsLinkHandler handler)
        {
            WorkItemLink link = handler.ParentLink;

            if (link != null)
            {
                return new[] { _elementFactory.CreateWorkItemLink(link.TargetId) };
            }
            return new CacheWorkItemLink[0];
        }

        private Dictionary<string, object> LoadValues(WorkItem toGetValues, CacheWorkItemType wiType)
        {
            Dictionary<string, object> values = new Dictionary<string, object>();

            foreach (CacheFieldDefinition fieldEntry in wiType.NonPredefinedFieldDefinitions)
            {
                values[fieldEntry.BackEndId] = toGetValues.TryGetValue<object>(fieldEntry.Schema, null);
            }
            return values;
        }

        #endregion

        #region Events
        //--------------------------------------------------------------------
        // Events
        //--------------------------------------------------------------------

        #endregion
    }
}